---
title: "Learner Bot — Memory Model"
type: concept
created: 2026-04-18
updated: 2026-04-18
sources: ["raw/articles/01-data-model.md", "raw/articles/02-api-contracts.md"]
tags: [learner-bot, memory, rag, vocab-gaps, curriculum-state]
---

# Memory Model

The [[Learner Bot]] maintains a persistent RAG memory store per child in the `learner_bot` database. This memory persists across reading sessions and nightly cycles — it is the bot's long-term knowledge about the child.

## Database Tables

### `learner_memories` — Core Memory Store

The primary memory table. Each row is one memory record of a specific type.

```sql
CREATE TABLE learner_memories (
  id           INT PRIMARY KEY AUTO_INCREMENT,
  learner_id   VARCHAR(36) NOT NULL,      -- universal UUID across all services
  memory_type  VARCHAR(100) NOT NULL,     -- one of 5 types below
  value        JSON NOT NULL,             -- typed payload
  confidence   DECIMAL(3,2) DEFAULT 1.00, -- 0.00 to 1.00
  created_at   DATETIME NOT NULL DEFAULT NOW(),
  updated_at   DATETIME NOT NULL DEFAULT NOW() ON UPDATE NOW(),
  INDEX(learner_id, memory_type)
);
```

### Memory Types

| Memory Type | What It Stores | Confidence Source |
|---|---|---|
| `assessment_result` | Quiz score, FK level, level_recommendation (drop/hold/raise) | 1.0 for completed quizzes; 0.0 for inferred |
| `curriculum_state` | Territory, year_level, mastered[], in_progress[] objective IDs | Based on quiz pass rates |
| `reading_pattern` | avg_time_per_page_ms, slow_pages[], pages_read/total_pages ratio | 0.90 if >80% pages read; 0.60 if partial session |
| `engagement_signal` | Signal type: `struggling` (slow_pages > 3), or `engaged` | Derived from reading_pattern |
| `vocabularyGaps` | Word tap counts — see `vocabulary_gaps` table | Proportional to tap frequency |

### `vocabulary_gaps` — Word Tap Accumulator

```sql
CREATE TABLE vocabulary_gaps (
  id           INT PRIMARY KEY AUTO_INCREMENT,
  learner_id   VARCHAR(36) NOT NULL,
  word         VARCHAR(255) NOT NULL,      -- lowercase
  tap_count    INT NOT NULL DEFAULT 1,
  last_tapped  DATETIME NOT NULL DEFAULT NOW(),
  UNIQUE(learner_id, word)                 -- upserts on conflict
);
```

Populated by `POST /api/v1/telemetry/vocab-taps` on session end. Upsert logic: `tap_count += count, last_tapped = NOW()`.

### `curriculum_state` — Curriculum Progress

```sql
CREATE TABLE curriculum_state (
  id           INT PRIMARY KEY AUTO_INCREMENT,
  learner_id   VARCHAR(36) NOT NULL UNIQUE,
  territory    VARCHAR(100) NOT NULL,       -- e.g., "England", "Turkey"
  year_level   INT NOT NULL,
  mastered     JSON NOT NULL DEFAULT '[]', -- objective IDs []
  in_progress  JSON NOT NULL DEFAULT '[]',
  updated_at   DATETIME NOT NULL DEFAULT NOW() ON UPDATE NOW()
);
```

### `assessment_results` — Quiz History

```sql
CREATE TABLE assessment_results (
  id                   INT PRIMARY KEY AUTO_INCREMENT,
  learner_id           VARCHAR(36) NOT NULL,
  book_id              VARCHAR(100) NOT NULL,
  score_pct            DECIMAL(5,2) NOT NULL,
  fk_level             DECIMAL(4,2) NULL,
  level_recommendation ENUM('drop','hold','raise') NULL,
  created_at           DATETIME NOT NULL DEFAULT NOW(),
  INDEX(learner_id)
);
```

### `nightly_reports` — Generated Reports

```sql
CREATE TABLE nightly_reports (
  id           INT PRIMARY KEY AUTO_INCREMENT,
  learner_id   VARCHAR(36) NOT NULL,
  report_type  ENUM('teacher','parent') NOT NULL,
  content_json JSON NOT NULL,
  generated_at DATETIME NOT NULL DEFAULT NOW(),
  INDEX(learner_id, report_type)
);
```

## Memory Flow Diagram

```mermaid
flowchart LR
    subgraph Writers
        T["Telemetry\n(session end)"]
        R["Reader App\n(quiz result)"]
        F["Fluency Assessment\n(UNVERIFIED — not yet wired)"]
        N["Nightly Cycle\n(04:00 UTC)"]
    end

    subgraph MemStore["learner_bot DB"]
        LM["learner_memories"]
        VG["vocabulary_gaps"]
        AR["assessment_results"]
        CS["curriculum_state"]
        NR["nightly_reports"]
    end

    subgraph Readers
        TP["Teacher Portal\n(via Bot API)"]
        PP["Parent Portal\n(via Bot API)"]
    end

    T --> LM
    T --> VG
    R --> AR
    F -.->|NOT YET WIRED| LM
    N --> NR
    N --> LM

    LM --> TP
    VG --> TP
    NR --> TP
    NR --> PP
```

## Who Reads Memory

⚠️ **Non-Negotiable (Sig):** Teachers and parents always read learner data via Learner Bot API — never via direct DB access.

- `GET /api/v1/bot/:learner_id/status` → Teacher Portal (full curriculum data)
- `GET /api/v1/bot/:learner_id/digest` → Parent Portal (warm-tone digest)

## Memory Lifetime

Per GDPR/data retention rules:
- `learner_memories`: retained 3 years (or until GDPR deletion request)
- `vocabulary_gaps`: retained 3 years
- `nightly_reports`: retained 3 years
- All deleted atomically when `POST /api/gdpr/delete` is called for that `learner_id`
